#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>

#include <modbus/modbus.h>
#include <modbus/modbus-tcp.h>
#include <modbus/modbus-rtu.h>

#include "gpio.h"

static void usage(const char *arg0) {
    printf("usage %s: option\n", arg0);

    printf("(*)-h  --hostname  <hostname>\n");
    printf("   -p  --port      <1-65535>       (default: 502)\n");

    printf("(*)-f  --device    <filename>      ttyS[0-3] ttyEXT[0-3]\n");
    printf("   -b  --baud      <9600,19200...> baud rate(default: 9600)\n");
    printf("       --parity    <N,O,E>         parity(None|Odd|Even, default: N)\n");
    printf("       --data_bits <5,6,7,8>       data bits(default: 8)\n");
    printf("       --stop_bits <0,1,2>         stop bits(default: 1)\n");
    printf("       --mode      <0,1>           mode(RS232|RS485, default: 0)\n");
    printf("       --rts       <0,1,2>         rts(NONE|UP|DOWN, default: 0)\n");
    printf("       --gpio      <0-255>         gpio(default: 0)\n");

    printf("   -i  --slave     <1-254>         slave id(default: 1)\n");
    printf("   -g  --range     <0,1,3,4>       range(default: 4)\n");
    printf("   -o  --offset    <0-65535>       offset(default: 0x0000)\n");
    printf("(*)-r  --read      <1-125>         read register quantity\n");
    printf("(*)-w  --write     <0-65535>       write register value\n");
}

enum modbus_type_e {
    mt_nvl, mt_rtu, mt_tcp
};

static int _gpio_fd = -1;

static void _modbus_set_rts(modbus_t *ctx, int on) {
    if (_gpio_fd <= 0) return;

    gpio_set_value(_gpio_fd, on);
}

static int _read(modbus_t *mb, int range, int addr, int nb) {
    int rc = -1;
    int i, j, k;

    uint8_t bits[MODBUS_MAX_READ_BITS];
    uint16_t regs[MODBUS_MAX_READ_REGISTERS];

    switch (range) {
        case 0:
        case 1:
            if (nb < 1 || nb >= MODBUS_MAX_READ_BITS) {
                fprintf(stderr, "nums must between 1 and %d\n", MODBUS_MAX_READ_BITS);
                return -1;
            }
            break;
        case 3:
        case 4:
            if (nb < 1 || nb >= MODBUS_MAX_READ_REGISTERS) {
                fprintf(stderr, "nums must between 1 and %d\n", MODBUS_MAX_READ_REGISTERS);
                return -1;
            }
            break;
        default:
            break;
    }

    switch (range) {
        case 0:
            rc = modbus_read_bits(mb, addr, nb, bits);
            printf("modbus_read_bits(%d, %d) = %d\n", addr, nb, rc);
            break;
        case 1:
            rc = modbus_read_input_bits(mb, addr, nb, bits);
            printf("modbus_read_input_bits(%d, %d) = %d\n", addr, nb, rc);
            break;
        case 3:
            rc = modbus_read_input_registers(mb, addr, nb, regs);
            printf("modbus_read_input_registers(%d, %d) = %d\n", addr, nb, rc);
            break;
        case 4:
            rc = modbus_read_registers(mb, addr, nb, regs);
            printf("modbus_read_registers(%d, %d) = %d\n", addr, nb, rc);
            break;
        default:
            break;
    }

    if (rc < 0) {
        return rc;
    }

    const int LINE = 0x10;
    int start = addr;
    int end = addr + nb;
    int base = start - start % LINE;

    printf("base_addr    ");
    for (i = 0; i < LINE; ++i) {
        printf("    %x", i);
    }
    printf("\n");

    switch (range) {
        case 0:
        case 1:
            for (i = 0, j = base; j < end; i += LINE, j += LINE) {
                printf("%04xH(%5d) ", j, j);
                for (k = 0; k < LINE; ++k) {
                    int curr = j + k;
                    if (curr >= start && curr < end) {
                        printf("  %s", bits[curr - start] ? " ON" : "off");
                    } else {
                        printf("     ");
                    }
                }
                printf("\n");
            }
            break;
        case 3:
        case 4:
            for (i = 0, j = base; j < end; i += LINE, j += LINE) {
                printf("%04xH(%5d) ", j, j);
                for (k = 0; k < LINE; ++k) {
                    int curr = j + k;
                    if (curr >= start && curr < end) {
                        printf(" %04x", regs[curr - start]);
                    } else {
                        printf("     ");
                    }
                }
                printf("\n");
            }
            break;
        default:
            break;
    }

    return rc;
}

static int _write(modbus_t *mb, int range, int addr, int val) {
    int rc = -1;

    switch (range) {
        case 0:
            rc = modbus_write_bit(mb, addr, val);
            printf("modbus_write_bit(%d, %d) = %d\n", addr, val, rc);
            break;
        case 4:
            rc = modbus_write_register(mb, addr, val);
            printf("modbus_write_register(%d, %d) = %d\n", addr, val, rc);
            break;
        default:
            break;
    }

    return rc;
}

int main(int argc, char *argv[]) {
    int rc = -1;
    int i;

    enum modbus_type_e mt = mt_nvl;

    char *hostname = "127.0.0.1";
    int port = 502;

    char device[64];
    int baud = 9600;
    char parity = 'N';
    int data_bits = 8;
    int stop_bits = 1;

    int mode = MODBUS_RTU_RS232;
    int rts = MODBUS_RTU_RTS_NONE;
    GPIO gpio = 0;

    int slave = 1;
    int range = 4;
    int addr = 0;

    if (1 == argc) {
        usage(argv[0]);
        return EXIT_SUCCESS;
    }

    for (i = 1; i < argc; ++i) {
        if (!strcmp("-h", argv[i]) || !strcmp("--hostname", argv[i])) {
            hostname = argv[++i];
            mt = mt_tcp;
            break;
        }
        if (!strcmp("-f", argv[i]) || !strcmp("--device", argv[i])) {
            sprintf(device, "/dev/%s", argv[++i]);
            mt = mt_rtu;
            break;
        }
    }

    for (i = 1; i < argc; ++i) {
        if (mt_tcp == mt) {
            if (!strcmp("-p", argv[i]) || !strcmp("--port", argv[i])) {
                port = atoi(argv[++i]);
            }
        } else if (mt_rtu == mt) {
            if (!strcmp("-b", argv[i]) || !strcmp("--baud", argv[i])) {
                baud = atoi(argv[++i]);
            }
            if (!strcmp("--parity", argv[i])) {
                parity = argv[++i][0];
            }
            if (!strcmp("--data_bits", argv[i])) {
                data_bits = atoi(argv[++i]);
            }
            if (!strcmp("--stop_bits", argv[i])) {
                stop_bits = atoi(argv[++i]);
            }
            if (!strcmp("--mode", argv[i])) {
                mode = atoi(argv[++i]);
            }
            if (!strcmp("--rts", argv[i])) {
                rts = atoi(argv[++i]);
            }
            if (!strcmp("--gpio", argv[i])) {
                gpio = atoi(argv[++i]);
            }
        }
    }

    for (i = 1; i < argc; ++i) {
        if (!strcmp("-i", argv[i]) || !strcmp("--slave", argv[i])) {
            slave = atoi(argv[++i]);
        }
        if (!strcmp("-g", argv[i]) || !strcmp("--range", argv[i])) {
            range = atoi(argv[++i]);
        }
        if (!strcmp("-o", argv[i]) || !strcmp("--offset", argv[i])) {
            addr = atoi(argv[++i]);
        }
    }

    modbus_t *mb = NULL;
    if (mt_tcp == mt) {
        mb = modbus_new_tcp(hostname, port);
        printf("modbus_new_tcp(%s, %d) = %p\n", hostname, port, mb);
    } else if (mt_rtu == mt) {
        mb = modbus_new_rtu(device, baud, parity, data_bits, stop_bits);
        printf("modbus_new_rtu(%s, %d, %d-%c-%d) = %p\n", device, baud, data_bits, parity, stop_bits, mb);
        modbus_rtu_set_serial_mode(mb, MODBUS_RTU_RS485);
        modbus_rtu_set_rts(mb, MODBUS_RTU_RTS_UP);
        modbus_rtu_set_custom_rts(mb, _modbus_set_rts);
    } else {
        fprintf(stderr, "'device' or 'hostname' must be specified\n");
        return EXIT_FAILURE;
    }

    if (NULL == mb) {
        return EXIT_FAILURE;
    }

    rc = modbus_connect(mb);
    if (rc < 0) {
        fprintf(stderr, "modbus_connect = %d\n", rc);
        return EXIT_FAILURE;
    }

    rc = modbus_set_slave(mb, slave);
    if (rc < 0) {
        fprintf(stderr, "modbus_set_slave = %d\n", rc);
        return EXIT_FAILURE;
    }

    rc = modbus_set_response_timeout(mb, 3, 0);
    if (rc < 0) {
        fprintf(stderr, "modbus_set_slave = %d\n", rc);
        return EXIT_FAILURE;
    }

    if (gpio > 0) {
        _gpio_fd = gpio_open(gpio, "direction");
    }

    for (i = 1; i < argc; ++i) {
        if (!strcmp("-r", argv[i]) || !strcmp("--read", argv[i])) {
            int nb = 1;
            if (i + 1 < argc) {
                nb = atoi(argv[++i]);
            }
            _read(mb, range, addr, nb);
        }

        if (!strcmp("-w", argv[i]) || !strcmp("--write", argv[i])) {
            int val = 0;
            if (i + 1 < argc) {
                val = atoi(argv[++i]);
            }
            _write(mb, range, addr, val);
        }
    }

    close(_gpio_fd);
    gpio_unexport(gpio);
    modbus_close(mb);
    modbus_free(mb);

    return EXIT_SUCCESS;
}
